/**
 * @file how_to_scan_images.cpp
 * @author 逆流 (1171267147@qq.com)
 * @brief 了解图像数据的存储和访问
 * @version 0.1
 * @date 2024-07-01
 *
 * @copyright Copyright (c) 2024
 * https://docs.opencv.org/4.x/db/da5/tutorial_how_to_scan_images.html
 */
// #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
// #include "doctest.h"
#include <cstring>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <sstream>

#include "opencv2/core/base.hpp"
#include "opencv2/core/hal/interface.h"
#include "opencv2/core/mat.hpp"
#include "opencv2/core/matx.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgcodecs.hpp"
// #include <fmt/core.h>

///************************************************************************
/// Q1: 如何访问图像像素
/// Q2: 矩阵数据如何存储
/// Q3: 如何测量程序性能
/// Q4: 如何使用查找表
///************************************************************************

///************************************************************************
/// 以一个颜色空间缩减算法为例：
/// 将单一颜色通道的值使用8为无符号数表示，那么值共有256种可能，为了减少颜色种类，
/// 我们可以将一段范围类的值映射为同一个值；
/// 比如将[0, 9]映射为10，[10, 19]映射为11，以此类推，计算公式如下：
/// $$
/// I_{new}=\lceil \frac{I_{old}}{10} \rceil \times 10
/// $$
/// 可以将上式中的 $10$ 替换为
/// $N$，$N$值越大，映射范围越大，所表示的颜色值越少。
/// 为了实现上述算法，如果依次编译每个像素点，对每个像素点都执行一次上述算法，成本太高；
/// 我们可以先将[0,
/// 255]每个值的映射存在一个表中，遍历到每个像素点时，直接根据像素值读取表中的映射值。
///************************************************************************

///************************************************************************
/// 图像矩阵存储布局如下：
/// 单通道图:
///       col0    col1   col...   colm
/// row0   [0,0]   [0,1]  ...
/// row1   [1,0]   [1,1]  ...
/// row... ...
///
/// 多通道图，以BGR格式为例：
///       col0           col1          col...   colm
/// row0   [0,0](B,G,R)   [0,1](B,G,R)  ...
/// row1   [1,0](B,G,R)   [1,1](B,G,R)  ...
/// row... ...
///
/// 实际存储中，是按行排列的方式连续存储，如果内存足够，这些行都是连续存储;
/// 连续内存不够时，可能会分布在不连续的内存块，可以通过
/// `cv::Mat::isContinuous()` 判断是否连续存储。
///************************************************************************
using namespace std;
using namespace cv;

static void Help() {
    cout << "\n----------------------------------------------------------------"
            "----------"
         << '\n'
         << "This program shows how to scan image objects in OpenCV (cv::Mat). "
            "As use case"
         << " we take an input image and divide the native color palette (255) "
            "with the "
         << '\n'
         << "input. Shows C operator[] method, iterators and at function for "
            "on-the-fly item address calculation."
         << '\n'
         << "Usage:" << '\n'
         << "./how_to_scan_images <imageNameToUse> <divideWith> [G]" << '\n'
         << "if you add a G parameter the image is processed in gray scale" << '\n'
         << "------------------------------------------------------------------"
            "--------"
         << '\n'
         << '\n';
}

/**
 * @brief 使用C风格指针遍历图像像素
 */
Mat& ScanImageAndReduceC(Mat& image, const uchar* table);
/**
 * @brief 使用迭代器遍历图像像素
 */
Mat& ScanImageAndReduceIterators(Mat& image, const uchar* table);
/**
 * @brief 使用像素坐标遍历图像像素
 */
Mat& ScanImageAndReduceRandomAccess(Mat& image, const uchar* table);

/// cv::getTickCount() 返回系统 CPU 从特定事件（例如自启动系统以来）的滴答次数
/// cv::getTickFrequency() 返回CPU在一秒内发出的滴答声次数

class Timer {
public:
    Timer() { frequency_ = getTickFrequency(); }
    ~Timer() = default;
    void Start() { start_ = static_cast<double>(getTickCount()); }
    void End() { end_ = static_cast<double>(getTickCount()); }

    double AverageTime(int times = 1) const { return (end_ - start_) / frequency_ / times * 1000 /*ms*/; }

private:
    double start_     = 0;
    double end_       = 0;
    double frequency_ = 0;
};

int main(int argc, char** argv) {
    Help();

    if (argc < 3) {
        cout << "Not enough parameters" << '\n';
        return -1;
    }

    Mat i;
    Mat j;
    if (argc == 4 && (strcmp(argv[3], "G") == 0)) {
        i = imread(argv[1], IMREAD_GRAYSCALE);
    } else {
        i = imread(argv[1], IMREAD_COLOR);
    }

    if (i.empty()) {
        cout << "Failed to load " << argv[1] << '\n';
        return -1;
    }

    const string kTitle = "GrayImage";
    namedWindow(kTitle, WINDOW_AUTOSIZE);
    imshow(kTitle, i);
    waitKey(0);

    int          divide_width = 0;
    stringstream s;
    s << argv[2];
    s >> divide_width;
    if (!s || (divide_width == 0)) {
        cout << "Invalid number entered for dividing." << '\n';
        return -1;
    }

    uchar table[256];
    for (int index = 0; index < 256; index++) {
        table[index] = (uchar)(divide_width * (index / divide_width));
    }

    const int kTimes = 100;

    Timer t;
    t.Start();

    for (int cnt = 0; cnt < kTimes; cnt++) {
        Mat clone_i = i.clone();
        j           = ScanImageAndReduceC(clone_i, table);
    }

    t.End();

    cout << "Time of reducing with the C operator [] (averaged for " << kTimes << " runs): " << t.AverageTime(kTimes)
         << " milliseconds." << '\n';
    imshow(kTitle, j);
    waitKey(0);

    t.Start();

    for (int cnt = 0; cnt < kTimes; cnt++) {
        Mat clone_i = i.clone();
        j           = ScanImageAndReduceIterators(clone_i, table);
    }

    t.End();
    cout << "Time of reducing with iterators (averaged for " << kTimes << " runs): " << t.AverageTime(kTimes)
         << " milliseconds." << '\n';
    imshow(kTitle, j);
    waitKey(0);

    t.Start();
    for (int cnt = 0; cnt < kTimes; cnt++) {
        Mat clone_i = i.clone();
        j           = ScanImageAndReduceRandomAccess(clone_i, table);
    }
    t.End();
    cout << "Time of reducing with random access (averaged for " << kTimes << " runs): " << t.AverageTime(kTimes)
         << " milliseconds." << '\n';
    imshow(kTitle, j);
    waitKey(0);

    // Opencv 提供LUT函数实现查找表功能，用于将图像中给定的像素值修改为其它值

    Mat    lookup_table(1, 256, CV_8U);
    uchar* p = lookup_table.ptr();
    for (int index = 0; index < 256; index++) {
        p[index] = table[index];
    }

    t.Start();
    for (int cnt = 0; cnt < kTimes; cnt++) {
        LUT(i, lookup_table, j);
    }

    t.End();
    cout << "Time of reducing with LUT (averaged for " << kTimes << " runs): " << t.AverageTime(kTimes)
         << " milliseconds." << '\n';
    imshow(kTitle, j);
    waitKey(0);

    return 0;
}

Mat& ScanImageAndReduceC(Mat& image, const uchar* table) {
    CV_Assert(image.depth() == CV_8U);

    int channels = image.channels();

    int rows = image.rows;
    int cols = image.cols * channels;

    // 如果内存连续，那么使用指针依次遍历是最高效的方式
    if (image.isContinuous()) {
        cols *= rows;
        rows = 1;
    }

    uchar* p = nullptr;

    for (int i = 0; i < rows; i++) {
        p = image.ptr<uchar>(i);
        for (int j = 0; j < cols; j++) {
            p[j] = table[p[j]];
        }
    }
    return image;
}

Mat& ScanImageAndReduceIterators(Mat& image, const uchar* table) {
    CV_Assert(image.depth() == CV_8U);

    const int kChannels = image.channels();
    switch (kChannels) {
        case 1: {
            // 迭代器是最安全的遍历方式，当数据不连续时，迭代器会自动处理数据间的"空洞"
            MatIterator_<uchar> it;
            MatIterator_<uchar> end;
            for (it = image.begin<uchar>(), end = image.end<uchar>(); it != end; ++it) {
                *it = table[*it];
            }
            break;
        }
        case 3: {
            MatIterator_<Vec3b> it;
            MatIterator_<Vec3b> end;
            for (it = image.begin<Vec3b>(), end = image.end<Vec3b>(); it != end; ++it) {
                (*it)[0] = table[(*it)[0]];
                (*it)[1] = table[(*it)[1]];
                (*it)[2] = table[(*it)[2]];
            }
            break;
        }
        default:
            break;
    }
    return image;
}

Mat& ScanImageAndReduceRandomAccess(Mat& image, const uchar* table) {
    CV_Assert(image.depth() == CV_8U);

    const int kChannels = image.channels();
    switch (kChannels) {
        case 1: {
            // 通过像素坐标对像素点进行随机访问
            for (int i = 0; i < image.rows; i++) {
                for (int j = 0; j < image.cols; j++) {
                    image.at<uchar>(i, j) = table[image.at<uchar>(i, j)];
                }
            }
            break;
        }
        case 2: {
            Mat_<Vec3b> image_bak = image;

            for (int i = 0; i < image.rows; i++) {
                for (int j = 0; j < image.cols; j++) {
                    image_bak(i, j)[0] = table[image_bak(i, j)[0]];
                    image_bak(i, j)[1] = table[image_bak(i, j)[1]];
                    image_bak(i, j)[2] = table[image_bak(i, j)[2]];
                }
            }
            break;
        }
        default:
            break;
    }
    return image;
}
